Implement support for closed TypedDicts (PEP 728)#21382
Conversation
bd9fee9 to
34df265
Compare
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
I'm not sure how exactly you'd prefer to have this change raised, so I've initially opened one big PR containing multiple small commits. Please LMK how you'd like it split up if that's the preference! |
|
First of all thanks for working on this!
This may depend on who exactly will review this PR :-) Although this is a big PR, after quick look it seems quite non-controversial, so the review will be mostly double-checking all edge cases are handled correctly/consistently. So IMO this doesn't need splitting into parts. @JukkaL @hauntsaninja will any of you have time to look at this? (I am not sure if I will have time/energy to do a proper review, as I still have a backlog of NumPy-related stuff.) |
PEP 728 allows multiple special forms to be applied to a key when a TypedDict is created using functional syntax, but the logic was only checking and stripping a single instance. This prevents overriding the overall total status on a ReadOnly field.
Fix a bug where a non-required key was considered consistent with a required key if the latter was marked as read-only.
Fix a bug where the meet of a mutable and readonly key was incorrectly set as readonly. This bug was caused by mistakenly unioning the readonly key sets, which happened to work for the tested case where readonly keys never appeared in the other type.
The TypedDict meet is returning Never whenever inputs have mismatched keys (value types or requiredness). However: * when a key is readonly, the meet value type can be a subtype of it * when a key is readonly and not required, the meet key can be required * when both keys are readonly and not required, the meet value type can be uninhabited (absent)
A typo was causing the readonly state of keys on the LHS of a TypedDict join to be ignored.
Improve TypedDict joins where keys mismatch by returning a readonly key with the joined type instead of discarding it.
Prior to implementing support for subclass refinement, expand the test suite with all the cases that need to be tested.
Allow readonly keys to be refined in subclasses, in line with PEP 705. For cases where refinement is not permitted by spec, provide a more detailed error message. As placeholders may now affect validation without ending up in the final TypedDictType, an `analysis_incomplete` field has been added, triggering reanalysis in subsequent rounds. This closes python#7435
The fallback path in TypeAnalyser::visit_typeddict_type is copying the required keys from the original type, but not the readonly keys. This is resulting in incorrect subtyping analysis for type variables with TypedDict bounds with readonly keys.
`NotRequired[Never]` can be used to indicate that a single key in a TypedDict will not be present.
Begin implementing PEP 728 support by adding an is_closed field to TypedDictType. This is filled out from the closed keyword, and displayed in reveal_type, but not otherwise supported yet.
Continue implementing PEP 728 support with updates to the subtyping logic. Drop the previous use of 'names_are_wider_than', which will be difficult to extend to cover `extra_items`, and instead use zipall to check for key addition/removal alongside the other key-based checks.
Propagate closed from subclasses. Verify that a subclass of a closed TypedDict does not add keys, nor override the closed status.
Close the join of two closed TypedDicts, and treat a missing key in a closed TypedDict as a `NotRequired[Never]` rather than the `ReadOnly[NotRequired[object]]` of an open TypedDict.
The meet of a closed TypedDict and another TypedDict must be closed. The implicit type of a missing key in a closed TypedDict is `ReadOnly[NotRequired[Never]]`.
Narrowing a union already treats final TypedDicts as closed; use the same logic if the closed keyword is used.
If a TypeVar with a TypedDict upper bound is encountered while narrowing, narrow based on the upper bound.
6734010 to
2d1333c
Compare
This comment has been minimized.
This comment has been minimized.
|
I accidentally force-pushed a complete rebase 🤦♀️ sorry, I think I managed to revert |
This comment has been minimized.
This comment has been minimized.
ilevkivskyi
left a comment
There was a problem hiding this comment.
Thanks for updates! Couple more comments (in addition to the typeddict_bases comment above).
| readonly_keys.update(base_typed_dict.readonly_keys) | ||
| return info, valid_items | ||
|
|
||
| def field_sources_in_reverse_mro( |
There was a problem hiding this comment.
This name is misleading, since we are only checking compatibility with immediate bases. And to be clear this is the right thing to do, since TypeDicts are structural and are constructed eagerly (i.e. they don't have an MRO).
| class A(TypedDict): | ||
| key: ReadOnly["X"] | ||
| class B(A): | ||
| key: "Y" # ok: Y subclasses X via YB |
There was a problem hiding this comment.
A random idea to make sure that our errors about incompatible overrides are consistent with TypedDict subtyping is to add assignments checks in (some of) those test cases where there are no errors. For example, here you may want to add:
x: A
y: B
x = y # Should be OK as well
ilevkivskyi
left a comment
There was a problem hiding this comment.
Few more comments (as I promised), just some minor things.
| defn.info | ||
| and defn.info.typeddict_type | ||
| and not has_placeholder(defn.info.typeddict_type) | ||
| and not defn.info.typeddict_type.analysis_incomplete |
There was a problem hiding this comment.
Using a flag instead of an extra has_placeholder() check(s) is a good idea, but same thing I mentioned before applies here, I would rather store this information on the TypeInfo. For example, something like info.typeddict_data.ready.
| ): | ||
| typeddict_type.analysis_incomplete = True | ||
| self.api.process_placeholder( | ||
| None, "TypedDict item", info, force_progress=typeddict_type != info.typeddict_type |
There was a problem hiding this comment.
I think force_progress should be updated as well, to match the logic few lines above.
| def primary_source(self, sources: list[FieldSource]) -> FieldSource: | ||
| """Select a primary source from a reverse-MRO-ordered list of sources. | ||
|
|
||
| The primary source will be the last in the MRO list, skipping readonly |
There was a problem hiding this comment.
Just noticed another place where you mention MRO. Instead say reverse order of immediate bases (or something similar).
| key in possible_iterable_type.items or not possible_iterable_type.is_final | ||
| ): | ||
| key in bound.items | ||
| and not isinstance(get_proper_type(bound.items[key]), UninhabitedType) |
There was a problem hiding this comment.
It would be good to add a reference to specific test cases in a comment here.
Despite the name, `TypeInfo` is a symbol (i.e. `SymbolNode`), while `TypedDictType` is a `ProperType`. 10-100x more types than symbols are created when type-checking typical code, so types should be kept "lightweight", as compared to symbols (when possible). Move the information needed to validate TypedDict field type inheritance to a new `typeddict_data` field on `TypeInfo`. Defer creating error messages to `TypeChecker` to avoid storing them unnecessarily.
We are only looking at immediate bases, so 'MRO' is misleading. Revert to previous framing as just "reverse order".
Make sure that our errors about incompatible overrides are consistent with TypedDict subtyping by adding assignment checks in some of the test cases where there are no errors.
This comment has been minimized.
This comment has been minimized.
ilevkivskyi
left a comment
There was a problem hiding this comment.
LG, thanks!
@alicederyn I have last small batch of minor comments.
@JukkaL @JelleZijlstra if you don't have other comments I will be merging this in the next few days.
| source = data.field_sources[field_name] | ||
| if source.base is None: | ||
| self.fail( | ||
| f'Definition of field "{field_name}" incompatible with base class "{base.name}"', |
There was a problem hiding this comment.
Could you please wrap long string manually (using implicit concatenation) here and below? For example:
| f'Definition of field "{field_name}" incompatible with base class "{base.name}"', | |
| f'Definition of field "{field_name}" incompatible | |
| f' with base class "{base.name}"', |
Also in semanal_typeddict.py. Unfortunately black doesn't do this automatically.
| from mypy.errorcodes import ErrorCode | ||
| from mypy.expandtype import expand_type | ||
| from mypy.exprtotype import TypeTranslationError, expr_to_unanalyzed_type | ||
| from mypy.message_registry import TYPEDDICT_OVERRIDE_MERGE |
There was a problem hiding this comment.
Is this constant still used anywhere? If not, you can just delete it. (Btw you can also put some of your error messages in message_registry if you want).
There was a problem hiding this comment.
It's still used in TypeAnalyser.visit_typeddict_type for experimental inline TypedDicts
| # candidates from the base classes, but it would be O(n^2) complexity | ||
| # to find out which is a supertype of all the others. Instead, use the | ||
| # first definition we encounter, and let the user provide the correct | ||
| # definition in the subclass if this fails. |
There was a problem hiding this comment.
This behavior is worth documenting (with a short example). It is not obvious to a user which base takes precedence: first or last.
| [typing fixtures/typing-typeddict.pyi] | ||
|
|
||
|
|
||
|
|
There was a problem hiding this comment.
Two empty lines between test blocks would be sufficient.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
|
Diff from mypy_primer, showing the effect of this PR on open source code: steam.py (https://github.com/Gobot1234/steam.py)
- steam/types/trade.py:69: error: Overwriting TypedDict field "instanceid" while merging [misc]
- steam/types/trade.py:69: error: Overwriting TypedDict field "classid" while merging [misc]
- steam/types/trade.py:112: error: Overwriting TypedDict field "assetid" while merging [misc]
- steam/types/trade.py:112: error: Overwriting TypedDict field "amount" while merging [misc]
- steam/types/trade.py:112: error: Overwriting TypedDict field "appid" while merging [misc]
- steam/types/trade.py:112: error: Overwriting TypedDict field "contextid" while merging [misc]
- steam/types/trade.py:112: error: Overwriting TypedDict field "instanceid" while merging [misc]
- steam/types/trade.py:112: error: Overwriting TypedDict field "classid" while merging [misc]
- steam/types/trade.py:112: error: Overwriting TypedDict field "missing" while merging [misc]
hydra-zen (https://github.com/mit-ll-responsible-ai/hydra-zen)
- src/hydra_zen/typing/_implementations.py:606: error: Overwriting TypedDict field "module" while extending [misc]
altair (https://github.com/vega/altair)
+ altair/vegalite/v6/schema/_config.py:6593: error: Unused "type: ignore" comment [unused-ignore]
+ altair/vegalite/v6/schema/_config.py:6618: error: Unused "type: ignore" comment [unused-ignore]
+ altair/vegalite/v6/api.py:699: error: Unused "type: ignore" comment [unused-ignore]
+ altair/vegalite/v6/api.py:725: error: Unused "type: ignore" comment [unused-ignore]
+ altair/vegalite/v6/api.py:774: error: Unused "type: ignore" comment [unused-ignore]
discord.py (https://github.com/Rapptz/discord.py)
- discord/types/emoji.py:42: error: Overwriting TypedDict field "animated" while extending [misc]
+ discord/types/scheduled_event.py:84: error: Field "user_count" is required in base class "_WithUserCount" but can be deleted in base class "StageInstanceScheduledEvent" [misc]
+ discord/types/scheduled_event.py:87: error: Field "user_count" is required in base class "_WithUserCount" but can be deleted in base class "VoiceScheduledEvent" [misc]
+ discord/types/scheduled_event.py:90: error: Field "user_count" is required in base class "_WithUserCount" but can be deleted in base class "ExternalScheduledEvent" [misc]
+ discord/types/interactions.py:214: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/interactions.py:220: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/interactions.py:226: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/interactions.py:232: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/interactions.py:234: error: Field "id" can be deleted in base class "ComponentBase" [misc]
+ discord/types/interactions.py:239: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/interactions.py:241: error: Field "id" can be deleted in base class "ComponentBase" [misc]
+ discord/types/interactions.py:246: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/interactions.py:248: error: Field "id" can be deleted in base class "ComponentBase" [misc]
+ discord/types/interactions.py:268: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/interactions.py:273: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:54: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:59: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:87: error: Definition of field "type" incompatible with base class "SelectComponent" [misc]
+ discord/types/components.py:92: error: Definition of field "type" incompatible with base class "SelectComponent" [misc]
+ discord/types/components.py:97: error: Definition of field "type" incompatible with base class "SelectComponent" [misc]
+ discord/types/components.py:102: error: Definition of field "type" incompatible with base class "SelectComponent" [misc]
+ discord/types/components.py:107: error: Definition of field "type" incompatible with base class "SelectComponent" [misc]
+ discord/types/components.py:113: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:125: error: Definition of field "type" incompatible with base class "SelectComponent" [misc]
+ discord/types/components.py:133: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:139: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:156: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:169: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:174: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:182: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:188: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:195: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:202: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:210: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:220: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
+ discord/types/components.py:232: error: Definition of field "type" incompatible with base class "ComponentBase" [misc]
- discord/types/scheduled_event.py:84: error: Overwriting TypedDict field "user_count" while merging [misc]
- discord/types/scheduled_event.py:87: error: Overwriting TypedDict field "user_count" while merging [misc]
- discord/types/scheduled_event.py:90: error: Overwriting TypedDict field "user_count" while merging [misc]
- discord/types/interactions.py:214: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/interactions.py:220: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/interactions.py:226: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/interactions.py:232: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/interactions.py:234: error: Overwriting TypedDict field "id" while extending [misc]
- discord/types/interactions.py:239: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/interactions.py:241: error: Overwriting TypedDict field "id" while extending [misc]
- discord/types/interactions.py:246: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/interactions.py:248: error: Overwriting TypedDict field "id" while extending [misc]
- discord/types/interactions.py:268: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/interactions.py:273: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/guild.py:142: error: Overwriting TypedDict field "stickers" while extending [misc]
- discord/types/components.py:54: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:59: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:87: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:92: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:97: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:102: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:107: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:113: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:125: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:133: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:139: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:156: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:169: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:174: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:182: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:188: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:195: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:202: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:210: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:220: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/components.py:232: error: Overwriting TypedDict field "type" while extending [misc]
- discord/types/gateway.py:180: error: Overwriting TypedDict field "newly_created" while extending [misc]
- discord/channel.py:144: error: Overwriting TypedDict field "bitrate" while extending [misc]
- discord/channel.py:145: error: Overwriting TypedDict field "user_limit" while extending [misc]
- discord/channel.py:146: error: Overwriting TypedDict field "rtc_region" while extending [misc]
- discord/channel.py:147: error: Overwriting TypedDict field "video_quality_mode" while extending [misc]
- discord/channel.py:148: error: Overwriting TypedDict field "overwrites" while extending [misc]
- discord/channel.py:151: error: Overwriting TypedDict field "topic" while extending [misc]
- discord/channel.py:152: error: Overwriting TypedDict field "slowmode_delay" while extending [misc]
- discord/channel.py:153: error: Overwriting TypedDict field "nsfw" while extending [misc]
- discord/channel.py:154: error: Overwriting TypedDict field "overwrites" while extending [misc]
- discord/channel.py:155: error: Overwriting TypedDict field "default_auto_archive_duration" while extending [misc]
- discord/channel.py:156: error: Overwriting TypedDict field "default_thread_slowmode_delay" while extending [misc]
+ discord/ext/commands/hybrid.py:61: error: Definition of field "description" incompatible with base class "_HybridCommandKwargs" [misc]
+ discord/ext/commands/hybrid.py:69: error: Definition of field "description" incompatible with base class "_HybridCommandDecoratorKwargs" [misc]
+ discord/ext/commands/hybrid.py:73: error: Definition of field "description" incompatible with base class "_HybridGroupKwargs" [misc]
- discord/ext/commands/hybrid.py:61: error: Overwriting TypedDict field "description" while extending [misc]
- discord/ext/commands/hybrid.py:64: error: Overwriting TypedDict field "with_app_command" while extending [misc]
- discord/ext/commands/hybrid.py:65: error: Overwriting TypedDict field "guild_ids" while extending [misc]
- discord/ext/commands/hybrid.py:66: error: Overwriting TypedDict field "guild_only" while extending [misc]
- discord/ext/commands/hybrid.py:67: error: Overwriting TypedDict field "default_permissions" while extending [misc]
- discord/ext/commands/hybrid.py:68: error: Overwriting TypedDict field "nsfw" while extending [misc]
- discord/ext/commands/hybrid.py:69: error: Overwriting TypedDict field "description" while extending [misc]
- discord/ext/commands/hybrid.py:73: error: Overwriting TypedDict field "description" while extending [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "max_messages" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "proxy" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "proxy_auth" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "shard_id" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "shard_count" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "application_id" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "member_cache_flags" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "chunk_guilds_at_startup" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "status" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "activity" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "allowed_mentions" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "heartbeat_timeout" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "guild_ready_timeout" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "assume_unsync_clock" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "enable_debug_events" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "enable_raw_presences" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "http_trace" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "max_ratelimit_timeout" while merging [misc]
- discord/ext/commands/bot.py:96: error: Overwriting TypedDict field "connector" while merging [misc]
pydantic (https://github.com/pydantic/pydantic)
- pydantic/fields.py:99: error: Unexpected keyword argument "closed" for "__init_subclass__" of "TypedDict" [call-arg]
|
|
@alicederyn Thanks for working on this! I guess next step is |
|
@alicederyn This is a great feature, thanks for implementing it! |
## Mypy 2.2
### Support for Closed TypedDicts (PEP 728)
Mypy now supports closed TypedDicts as specified in PEP 728. A closed TypedDict cannot have extra
keys beyond those explicitly defined. This allows the type checker to determine that certain
operations are safe when they otherwise wouldn't be due to the potential presence of unknown keys.
You can use the `closed` keyword argument with `TypedDict`:
```python
HasName = TypedDict("HasName", {"name": str})
HasOnlyName = TypedDict("HasOnlyName", {"name": str}, closed=True)
Movie = TypedDict("Movie", {"name": str, "year": int})
movie: Movie = {"name": "Nimona", "year": 2023}
has_name: HasName = movie # OK: HasName is open (default)
has_only_name: HasOnlyName = movie # Error: HasOnlyName is closed and Movie has extra "year" key
```
Closed TypedDicts enable more precise type checking because the type checker knows exactly which
keys are present. This is particularly useful when working with TypedDict unions or when you want
to ensure that a TypedDict conforms to an exact shape.
The `closed` keyword also enables safe type narrowing with `in` checks:
```python
Book = TypedDict('Book', {'book': str}, closed=True)
DVD = TypedDict('DVD', {'dvd': str}, closed=True)
type Inventory = Book | DVD
def print_type(inventory: Inventory) -> None:
if "book" in inventory:
# Type is narrowed to Book here - safe because DVD is closed
print(inventory["book"])
else:
# Type is narrowed to DVD here
print(inventory["dvd"])
```
The `closed` keyword is also supported in class-based syntax:
```python
class HasOnlyName(TypedDict, closed=True):
name: str
```
Note that closed TypedDicts are structural types, so a closed TypedDict is assignable to an open
TypedDict with the same keys, but not vice versa.
Contributed by Alice (PR [21382](python/mypy#21382)).
### Complete Support for Type Variable Defaults (PEP 696)
Mypy now has complete support for type variable defaults as specified in PEP 696. This allows you to
specify default values for type parameters in generic classes, functions, and type aliases.
Traditional syntax (Python 3.11 and earlier):
```python
T = TypeVar("T", default=int) # This means that if no type is specified T = int
@DataClass
class Box(Generic[T]):
value: T | None = None
reveal_type(Box()) # type is Box[int]
reveal_type(Box(value="Hello World!")) # type is Box[str]
```
New syntax (Python 3.12+):
```python
class Box[T = int]:
def __init__(self, value: T) -> None:
self.value = value
reveal_type(Box()) # type is Box[int]
reveal_type(Box(value="Hello World!")) # type is Box[str]
```
Type variable defaults work with all forms of generics, including classes, functions, and type aliases.
This release completes the implementation by fixing various edge cases involving recursive defaults,
dependencies between type variables, and interactions with variadic generics.
Contributed by Ivan Levkivskyi (PRs [21491](python/mypy#21491),
[21526](python/mypy#21526), [21544](python/mypy#21544)).
### Respect Explicit Return Type of `__new__()`
Mypy now respects explicitly annotated return types in `__new__()` methods. Previously, mypy would
always assume that `__new__()` returns an instance of the current class, ignoring explicit annotations.
With this change, if you explicitly annotate a return type that differs from the implicit type, mypy
will use the explicit annotation:
```python
class Factory:
def __new__(cls) -> Product:
return Product()
reveal_type(Factory()) # type is Product, not Factory
```
Note that mypy still gives an error at the definition site if the explicit annotation is not a
subtype of the current class, since this is technically not type-safe.
For backwards compatibility, there are two exceptions:
- If the return type is `Any`, mypy will still use the current class as the return type.
- If the explicit return type comes from a superclass and is a supertype of the implicit return type,
mypy will use the implicit (more specific) type:
```python
class A:
def __new__(cls) -> A: ...
reveal_type(A()) # type is A
class B:
def __new__(cls) -> B:
return cls()
class C(B): ...
reveal_type(C()) # type is C
```
This fixes several long-standing issues where explicit `__new__()` return types were ignored.
Contributed by Ivan Levkivskyi (PR [21441](python/mypy#21441)).
### TypeForm Support No Longer Experimental
Support for `TypeForm` is no longer experimental. `TypeForm` (introduced in Python 3.14) allows you
to annotate parameters that accept type expressions, providing better type checking for functions
that work with types as values.
```python
from typing import TypeForm
def make_list(tp: TypeForm[T]) -> list[T]:
...
# Correctly typed as list[int]
int_list = make_list(int)
```
`TypeForm` support was previously reverted from mypy 2.1 due to a performance regression, but this
has now been mitigated.
Contributed by Ivan Levkivskyi and Jelle Zijlstra (PRs [21262](python/mypy#21262), [21591](python/mypy#21591),
[21459](python/mypy#21459)).
### Experimental WASM Wheel for Python 3.14
Mypy now ships an experimental WebAssembly (WASM) wheel for Python 3.14. This allows mypy to run
in WASM environments such as Pyodide and browser-based Python implementations.
The WASM wheel is considered experimental and may have limitations compared to native builds. Please
report any issues you encounter when using mypy in WASM environments.
Contributed by Ivan Levkivskyi (PR [21671](python/mypy#21671)).
### Mypyc Free-threading Improvements
- Make function wrappers thread-safe on free-threaded builds (Jukka Lehtosalo, PR [21620](python/mypy#21620))
- Make list remove and index thread-safe on free-threaded builds (Jukka Lehtosalo, PR [21614](python/mypy#21614))
- Fix dict iteration memory safety on free-threaded builds (Jukka Lehtosalo, PR [21617](python/mypy#21617))
- Make some dict primitives thread-safe on free-threading builds (Jukka Lehtosalo, PR [21616](python/mypy#21616))
- Fix free-threading race condition in argument parsing (Jukka Lehtosalo, PR [21613](python/mypy#21613))
- Document free threading and other doc updates (Jukka Lehtosalo, PR [21494](python/mypy#21494))
### `librt.strings` Updates
- Add `librt.strings.toupper` and `librt.strings.tolower` codepoint primitives (Vaggelis Danias, PR [21553](python/mypy#21553))
- Add `librt.strings.isidentifier` codepoint primitive (Vaggelis Danias, PR [21522](python/mypy#21522))
- Add `librt.strings.isalpha` codepoint primitive (Vaggelis Danias, PR [21521](python/mypy#21521))
- Add `librt.strings.isalnum` codepoint primitive (Vaggelis Danias, PR [21509](python/mypy#21509))
- Add `librt.strings.isdigit` codepoint primitive (Vaggelis Danias, PR [21504](python/mypy#21504))
- Add `librt.strings.isspace` char primitive (Vaggelis Danias, PR [21462](python/mypy#21462))
### Mypyc Improvements
- Fix name lookup when class var and module var have the same name (Jukka Lehtosalo, PR [21594](python/mypy#21594))
- Report file and line number on uncaught exceptions (Jukka Lehtosalo, PR [21584](python/mypy#21584))
- Use `other` arg instead of `self` for RHS type (Ryan Heard, PR [21569](python/mypy#21569))
- Use `method_sig` to get the method signature (Ryan Heard, PR [21567](python/mypy#21567))
- Preserve inherited attribute defaults under `separate=True` (Jo, PR [21547](python/mypy#21547))
- Fix missing cross-group header deps in incremental builds (Jo, PR [21490](python/mypy#21490))
- Fix cross-group call to inherited `__mypyc_defaults_setup` (Jo, PR [21481](python/mypy#21481))
- Fix non-deterministic class struct layout under `separate=True` (Vaggelis Danias, PR [21530](python/mypy#21530))
- Specialize `s[i] == 'x'` to a codepoint int compare (Vaggelis Danias, PR [21579](python/mypy#21579))
- Fix reference leak in mypyc bytes concatenation (Colinxu2020, PR [21469](python/mypy#21469))
### Fixes to Crashes
- Fix crash on invalid recursive variadic alias (Ivan Levkivskyi, PR [21572](python/mypy#21572))
- Fix crashes on variadic unpacking in synthetic types (Ivan Levkivskyi, PR [21555](python/mypy#21555))
- Fix crash on unhandled meet variadic tuple vs instance (Ivan Levkivskyi, PR [21558](python/mypy#21558))
- Fix crash on deferred generic class nested in function (Ivan Levkivskyi, PR [21557](python/mypy#21557))
- Fix crash in new-style type alias with variadic unpack (Ivan Levkivskyi, PR [21551](python/mypy#21551))
- Fix various crashes on recursive type variable defaults (Ivan Levkivskyi, PR [21491](python/mypy#21491))
- Fix crash for empty `Annotated` type application (Rayan Salhab, PR [21503](python/mypy#21503))
- Fix crash on `Unpack` used without arguments in class bases (Sai Asish Y, PR [21470](python/mypy#21470))
### Performance Improvements
- Memoize the options snapshot (Kevin Kannammalil, PR [21354](python/mypy#21354))
- Don't include `not_ready_deps` tracking as relating to mypy internals (Kevin Kannammalil, PR [21389](python/mypy#21389))
- Speed up transitive dependency hash for singleton SCCs (Kevin Kannammalil, PR [21390](python/mypy#21390))
- Optimize typeform checks (Jelle Zijlstra, PR [21459](python/mypy#21459))
### Improvements to the Native Parser
- Support `--shadow-file` with `--native-parser` (Jukka Lehtosalo, PR [21623](python/mypy#21623))
- Add Python version checks to native parser (Kevin Kannammalil, PR [21539](python/mypy#21539))
- Allow nativeparse to parse source code directly (bzoracler, PR [21260](python/mypy#21260))
### Other Notable Fixes and Improvements
- Add function definition notes for `too many positional arguments` errors (Kevin Kannammalil, PR [21410](python/mypy#21410))
- Fix the exportjson tool (.ff cache to .json conversion) (Jukka Lehtosalo, PR [21628](python/mypy#21628))
- Support floats in JSON in fixed-format cache (Ivan Levkivskyi, PR [21603](python/mypy#21603))
- Update `TypedDictType.__init__` signature to preserve backward compat (Jukka Lehtosalo, PR [21590](python/mypy#21590))
- Fix constructor calls for union-bounded `TypeVar`s (Jingchen Ye, PR [21571](python/mypy#21571))
- Fix `TypedDict` indexing with literal keys in comprehensions (Jingchen Ye, PR [21556](python/mypy#21556))
- Correctly handle empty tuple index when unpacked (Ivan Levkivskyi, PR [21545](python/mypy#21545))
- Support protocol checks for self-types in tuple types (Ivan Levkivskyi, PR [21535](python/mypy#21535))
- Fix edge cases in variadic tuple subclasses (Ivan Levkivskyi, PR [21518](python/mypy#21518))
- Special-case constructor for tuple types (Ivan Levkivskyi, PR [21502](python/mypy#21502))
- Fix false positive "Expected TypedDict key to be string literal" for `Union[TypedDict, dict[K, V]]` (Zakir Jiwani, PR [21511](python/mypy#21511))
- Use explicit `Never` for type inference (Ivan Levkivskyi, PR [21497](python/mypy#21497))
- Narrow membership in statically known containers (Shantanu, PR [21461](python/mypy#21461))
- Improve negative narrowing for membership checks on tuples (Shantanu, PR [21456](python/mypy#21456))
- Analyze `TypedDict` decorators (Pranav Manglik, PR [21267](python/mypy#21267))
- Start testing Python 3.15 (Marc Mueller, PR [21439](python/mypy#21439))
- Improved handling of `NamedTuple`, `TypedDict`, `Enum`, and regular classes nested in functions (Ivan Levkivskyi, PR [21478](python/mypy#21478))
Implement support for the closed keyword on TypedDicts (part of PEP 728).
Additionally, fix some preexisting issues that I came across while updating the logic.
Overwriting TypedDict field "x" while merging#8714